﻿using System;
using System.Globalization;
using AutoFixture.Kernel;
using NSubstitute.Core;
using NSubstitute.Exceptions;

namespace AutoFixture.AutoNSubstitute
{
    /// <summary>
    /// Relays a request for an interface or an abstract class to a request for a substitute of that type.
    /// </summary>
    /// <remarks>
    /// This class serves as a residue collector, catching unanswered requests for an instance of an abstract type and
    /// converting them to requests for a substitute of that type, dynamically generated by NSubstitute.
    /// </remarks>
    /// <seealso cref="IFixture.ResidueCollectors"/>
    public class SubstituteRelay : ISpecimenBuilder
    {
        /// <summary>
        /// Specification used to check whether a type request should be redirected to a substitute request.
        /// </summary>
        public IRequestSpecification Specification { get; }

        /// <summary>
        /// Creates a new instance of <see cref="SubstituteRelay"/>.
        /// </summary>
        /// <param name="specification">Specification to test whether request should be relayed.</param>
        public SubstituteRelay(IRequestSpecification specification)
        {
            this.Specification = specification ?? throw new ArgumentNullException(nameof(specification));
        }

        /// <summary>
        /// Creates a new instance of <see cref="SubstituteRelay"/>.
        /// </summary>
        public SubstituteRelay()
            : this(new AbstractTypeSpecification())
        {
        }

        /// <summary>
        /// Creates a substitute when request is an abstract type.
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// An attempt to resolve a substitute from the <paramref name="context"/> returned an object that was not
        /// created by NSubstitute.
        /// </exception>
        public object Create(object request, ISpecimenContext context)
        {
            if (context == null) throw new ArgumentNullException(nameof(context));

            if (!this.Specification.IsSatisfiedBy(request))
                return new NoSpecimen();

            var requestedType = request as Type;
            if (requestedType == null)
                return new NoSpecimen();

            object substitute = context.Resolve(new SubstituteRequest(requestedType));

            try
            {
                SubstitutionContext.Current.GetCallRouterFor(substitute);
            }
            catch (NotASubstituteException e)
            {
                throw new InvalidOperationException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "Object resolved by request for substitute of {0} was not created by NSubstitute. " +
                        "Ensure that {1} was added to Fixture.Customizations.",
                        requestedType.FullName, typeof(SubstituteRequestHandler).FullName),
                    e);
            }

            return substitute;
        }
    }
}
